This script performs preprocessing and quality control (QC) filtering of single-cell RNA sequencing (scRNA-seq) data generated using conventional 10x Genomics, FLEX, and Parse Biosciences Mini and Midi kits. The R package CRMetrics is utilized for QC in this script. While the published version of CRMetrics (https://github.com/khodosevichlab/CRMetrics) is only compatible with conventional 10x Genomics data, a local adaptation has been implemented to support 10x FLEX and Parse Biosciences data. Additionally, this version incorporates compatibility with the latest CellBender release (version 0.3.0). For large datasets (~ 100,000 nuclei) memory limitations of R (e.g. with Conos and CRMetrics) necessitate transitioning to Python for data analysis. More information and application of the CRMetrics package can be found here: http://kkh.bric.ku.dk/rasmus/CRMetrics/walkthrough.html

QC filtering involves iterative adjustments to determine the optimal thresholds for filtering depth (unique molecular identifiers [UMIs] per cell), mitochondrial (mito) fraction (percentage of mitochondrial gene expression per cell), and doublets (two or more cells clustered together). Final clustering is not conducted in this script, it is addressed in a subsequent script, “Clustering_and_annotation”.

If ambient RNA filtering is applied, it is performed first, followed by filtering based on depth, mitochondrial fraction, and doublets. At the end of the pipeline, a new CRMetrics object is constructed using the filtered count matrices (=cms) to plot the violin plots for UMI and gene counts post-filtering.

Due to its computational intensity, ambient RNA filtering is not part of the standard analysis pipeline but is performed upon specific user request.

Setup

library(devtools)
# loading locally adapted CRMetrics version
#load_all("/people/nrq364/CRMetrics_LW")
library(CRMetrics)
library(magrittr)
library(dplyr)
library(conos)
library(pagoda2)
library(qs)

1. fastQC and multiQC

Provide links to multiQC reports of raw sequencing data and cellranger metrics. Does not yet work on cellranger multi (FLEX) and Parse output.

2. Create CRMetrics object

In case of Parse data use data.path to “combined” folder, for 10x FLEX data.path to “per_sample_outs” folder. Metadata should at least contain “sample” and “group/condition” but can have more columns.

crm <- CRMetrics$new(data.path = "/data/counts/[project_name]/", 
                     metadata = "path_to_metadata.csv", 
                     n.cores = 60)
crm$metadata
qsave(crm, "crm.qs", nthreads = 10)

3. Basic metrics

Show available metrics to plot.

crm$selectMetrics()

Plot selected metrics per sample

e.g. cells per sample, median genes, median UMIs, mean reads

metrics.to.plot <- crm$selectMetrics(ids = c(1:3,5,6))
crm$plotSummaryMetrics(plot.geom = "bar", comp.group = "sample", metrics = metrics.to.plot)

Plot selected metrics per condition

We can do the same, but set the comparison group to condition. This will add statistics to the plots. Additionaly, we can add a second comparison group for coloring.

crm$plotSummaryMetrics(comp.group = "condition",
                       metrics = metrics.to.plot, 
                       plot.geom = "point", 
                       stat.test = "non-parametric") #second.comp.group= "sex"

Add and pot detailed metrices

crm$addCms(add.metadata=F) #for Parse data: parse = T, for FLEX: flex = T
crm$addDetailedMetrics(n.cores = 30)

get total number of sequenced nuclei

sum(sapply(crm$cms, function(d) dim(d)[2]))
[1] 61975

plot UMIs and genes per sample before filtering, horizontal lines indicate the median values for all samples

crm$plotDetailedMetrics(comp.group = "condition",
                        metrics = c("UMI_count",  "gene_count"), 
                        plot.geom = "violin")

Generate a data frame with the metrics.

metrics <- crm$summary.metrics
metrics %<>% as.data.frame()
metrics

Get median genes per cell across all samples before filtering (in case of Parse: “hg38_median_genes_per_cell”).

median(metrics[metrics$metric=="median genes per cell",]$value)
[1] 3478

Get median UMIs per cell across all samples before filtering (in case of Parse: “hg38_median_tscp_per_cell”).

median(metrics[metrics$metric=="median umi counts per cell",]$value)
[1] 7945

If ambient RNA removal is part of the pipeline, it is performed here. Relevant code is located further down in the script under 8.

4. Embed cells using conos

To plot the cells in an embedding, we need to preprocess the raw count matrices with pagoda2 (seurat is also possible).

crm$doPreprocessing() #min.transcripts.per.cell = 100

Create embedding using conos. This functions executes the functions Conos$new(cms, n.cores = n.cores), buildGraph(), findCommunities(), embedGraph(method = “UMAP”), with default settings, which can be adjusted.

crm$createEmbedding()

Plot the embedding. This is before filtering, so we are not yet interested in the clusters, those are refined in the next script.

crm$plotEmbedding()

5. Filtering out low quality cells

Cell depth

Plot cell depth in the embedding or as histograms per sample.

crm$plotEmbedding(depth = TRUE, 
             depth.cutoff = 1000)

crm$plotDepth()

If the depth distribution varies between samples, we can use sample-specific cutoffs.

depth_cutoff_vec <- c(1e3, 1e3, 500, 1e3, 1e3, 500, 1e3, 1e3, 1e3, 1e3, 1e3, 500) %>% 
  setNames(crm$detailed.metrics$sample %>% 
             unique() %>% 
             sort())

depth_cutoff_vec
control_1 control_2 control_4 control_5 control_6 control_7 treated_1 treated_2 treated_3 treated_4 
     1000      1000       500      1000      1000       500      1000      1000      1000      1000 
treated_6 treated_7 
     1000       500 

Plot sample specific cutoff in histogram and embedding.

crm$plotDepth(cutoff = depth_cutoff_vec)

crm$plotEmbedding(depth = TRUE, 
             depth.cutoff = depth_cutoff_vec)

Mitochondrial gene fraction

Investigate the mitochondrial gene fraction in the cells. The example dataset has a very low expression of mitochondrial genes. Usual cutoff is 5% or higher.

crm$plotEmbedding(mito.frac = TRUE, 
             mito.cutoff = 0.01, 
             species = "mouse")

We can plot the distribution of the mitochondrial fraction per sample and also include sample-wise cutoffs (not shown here as the example dataset has only a very low expression of mitochondrial genes).

Doublet detection

Doublet detection within CRMetrics is possible with the python packages scrublet or DoubletDetection Scrublet is the default method, which is fast. DoubletDetection is significantly slower, but performs better according to this review (https://www.sciencedirect.com/science/article/pii/S2405471220304592). Whcih one to choose depends on the dataset and technology used.

Here we use scrublet as default method. A virtual environment is used to install scrublet.

library(reticulate)
conda_create("scrublet", 
             conda = "/opt/software/miniconda/4.12.0/condabin/conda", 
             python_version = 3.8)

Run scrublet from within R or generate a Python script to run it from the terminal. To do this, set export = TRUE and run python scrublet.py inside the virtual environment to generate data.

crm$detectDoublets(env = "scrublet",
                   conda.path = "/opt/software/miniconda/4.12.0/condabin/conda",
                   method = "scrublet")

Then, load data with:

crm$addDoublets()

Plot the estimated doublets in the embedding.

crm$plotEmbedding(doublet.method = "scrublet")

Plot filtered cells

Plot all cells to be filtered on the embedding.

crm$plotFilteredCells(type = "embedding", 
                      depth = TRUE, 
                      depth.cutoff = 1000, 
                      doublet.method = "scrublet", 
                      mito.frac = FALSE, 
                      species = "mouse")
Scale for colour is already present.
Adding another scale for colour, which will replace the existing scale.

Plot the cells to be filtered per sample in a bar plot where combination means a cell that has at least two filter labels, e.g. doublet and depth.

crm$plotFilteredCells(type = "bar", 
                      depth = TRUE, 
                      depth.cutoff = 1000, 
                      doublet.method = "scrublet", 
                      mito.frac = FALSE, 
                      species = "mouse")

6. Filter and save count matrices

Finally, filter the count matrices to create a cleaned list to be used in downstream applications. If cellbender should be used then cms needs to be added after cellbender and then all subsequent filtering is done.

crm$filterCms(depth.cutoff = 1000, 
              mito.cutoff = NULL, 
              doublets = "scrublet",
              samples.to.exclude = NULL,
              species = "mouse",
              verbose = TRUE)

The filtered list of count matrices is stored in $cms.filtered which can be saved on disk.

qsave(crm$cms.filtered, "cms_filtered[_cellbender].qs", 
      nthreads = 10)

7. Plot detailed metrics after filtering

cms <- qread("cms_filtered[_cellbender].qs")

Create a new crm object with cms after filtering.

crm_filtered <- CRMetrics$new(cms = crm$cms.filtered, n.cores = 60, metadata="path_to_metadata.csv")
crm_filtered$addSummaryFromCms()
crm_filtered$addDetailedMetrics(n.cores = 30)

Total number of nuclei retained after QC:

sum(sapply(crm_filtered$cms, function(d) dim(d)[2]))

Plot UMIs and genes per sample after QC filtering, horizontal lines indicate the median values for all samples.

crm_filtered$plotDetailedMetrics(comp.group = "condition",
                        metrics = c("UMI_count",  "gene_count"), 
                        plot.geom = "violin")

Generate a data frame with the metrics.

metrics <- crm_filtered$summary.metrics
metrics %<>% as.data.frame()
metrics

Median genes per cell across all samples after QC filtering (in case of Parse: “hg38_median_genes_per_cell”).

median(metrics[metrics$metric=="median genes per cell",]$value)

Median UMIs per cell across all samples after QC filtering (in cas of Parse: “hg38_median_tscp_per_cell”).

median(metrics[metrics$metric=="median umi counts per cell",]$value)

8. Optional: remove ambient RNA

We use usually cellbender, SoupX is also included in CRMetrics. Perform this step before filtering, it changes the number of called cells and their gene counts.

In the old cellbender version (before 0.3.0) we needed to specify expected number of cells and total droplets included. As hinted in the manual, the number of total droplets included could be expected number of cells multiplied by 3, this can still be plotted here when setting show.total.droplets=T. Additionally, in case of Parse, the following function creates the input files for cellbender from the Parse output.

crm$prepareCellbender(show.expected.cells = T, show.total.droplets = T)
2025-04-30 10:32:28.050553 Started run using 12 cores
2025-04-30 10:32:28.057862 Using stored HDF5 Cell Ranger/Parse outputs. To overwrite, set $cms.raw <- NULL
2025-04-30 10:32:28.058687 Using stored UMI counts calculations. To overwrite, set $cellbender$umi.counts <- NULL
2025-04-30 10:32:28.059399 Plotting
2025-04-30 10:32:28.885673 Done!

The following function prepares and saves the cellbender script. To only select specific samples, use the samples parameter. total.droplets and expected.cells is a logical because in cellbender from v0.3.0 total.droplets and expected.cells do not need to be specified anymore.
○ FALSE is default
○ TRUE would take expected.cells from metadata and total droplets would be 3x expected cells
○ alternatively give own numbers (named vector)

crm$saveCellbenderScript(file = "cellbender_script.sh", 
                         fpr = 0.01, 
                         epochs = 150, 
                         use.gpu = TRUE, total.droplets = F, expected.cells = F) # could be total.droplets = droplets (named vector)

We can run this script in the terminal (screen session). Here, we activate the environment: conda activate cellbender_v0.3.0 and we run the bash script: sh cellbender_script.sh.

Plot cellbender results

Plot the changes in cell numbers following CellBender estimations.

crm$plotCbCells()

Plot the CellBender training results.

crm$plotCbTraining()

Plot the cell probabilities.

crm$plotCbCellProbs()

Plot the identified ambient genes per sample.

crm$plotCbAmbExp(cutoff = 0.005)

Lastly, plot the proportion of samples expressing ambient genes. MALAT1 is identified as an ambient gene in almost all samples which is expected.

crm$plotCbAmbGenes(cutoff = 0.005)

Add cellbender filtered cms

Then, we can add the cellbender filtered cms to our object. Additionally, it is recommended to generate new summary metrics from the adjusted cms which can then be plotted.

crm$cms <- NULL
crm$addCms(cellbender = T)
crm$addSummaryFromCms()
crm$detailed.metrics <- NULL
crm$addDetailedMetrics()
qsave(crm, "crm_cellbender.qs")
LS0tCnRpdGxlOiAiMSkgUHJlcHJvY2Vzc2luZyBhbmQgUUMgZmlsdGVyaW5nIgpkYXRlOiAiYHIgU3lzLkRhdGUoKWAiCm91dHB1dDoKICBodG1sX25vdGVib29rOgogICAgdG9jOiB0cnVlCiAgICB0b2NfZmxvYXQ6IHRydWUKICAgIGNvZGVfZm9sZGluZzogbm9uZQotLS0KClRoaXMgc2NyaXB0IHBlcmZvcm1zIHByZXByb2Nlc3NpbmcgYW5kIHF1YWxpdHkgY29udHJvbCAoUUMpIGZpbHRlcmluZyBvZiBzaW5nbGUtY2VsbCBSTkEgc2VxdWVuY2luZyAoc2NSTkEtc2VxKSBkYXRhIGdlbmVyYXRlZCB1c2luZyBjb252ZW50aW9uYWwgMTB4IEdlbm9taWNzLCBGTEVYLCBhbmQgUGFyc2UgQmlvc2NpZW5jZXMgTWluaSBhbmQgTWlkaSBraXRzLgpUaGUgUiBwYWNrYWdlIENSTWV0cmljcyBpcyB1dGlsaXplZCBmb3IgUUMgaW4gdGhpcyBzY3JpcHQuIFdoaWxlIHRoZSBwdWJsaXNoZWQgdmVyc2lvbiBvZiBDUk1ldHJpY3MgKGh0dHBzOi8vZ2l0aHViLmNvbS9raG9kb3NldmljaGxhYi9DUk1ldHJpY3MpIGlzIG9ubHkgY29tcGF0aWJsZSB3aXRoIGNvbnZlbnRpb25hbCAxMHggR2Vub21pY3MgZGF0YSwgYSBsb2NhbCBhZGFwdGF0aW9uIGhhcyBiZWVuIGltcGxlbWVudGVkIHRvIHN1cHBvcnQgMTB4IEZMRVggYW5kIFBhcnNlIEJpb3NjaWVuY2VzIGRhdGEuIEFkZGl0aW9uYWxseSwgdGhpcyB2ZXJzaW9uIGluY29ycG9yYXRlcyBjb21wYXRpYmlsaXR5IHdpdGggdGhlIGxhdGVzdCBDZWxsQmVuZGVyIHJlbGVhc2UgKHZlcnNpb24gMC4zLjApLiBGb3IgbGFyZ2UgZGF0YXNldHMgKH4gMTAwLDAwMCBudWNsZWkpIG1lbW9yeSBsaW1pdGF0aW9ucyBvZiBSIChlLmcuIHdpdGggQ29ub3MgYW5kIENSTWV0cmljcykgbmVjZXNzaXRhdGUgdHJhbnNpdGlvbmluZyB0byBQeXRob24gZm9yIGRhdGEgYW5hbHlzaXMuIE1vcmUgaW5mb3JtYXRpb24gYW5kIGFwcGxpY2F0aW9uIG9mIHRoZSBDUk1ldHJpY3MgcGFja2FnZSBjYW4gYmUgZm91bmQgaGVyZTogaHR0cDovL2traC5icmljLmt1LmRrL3Jhc211cy9DUk1ldHJpY3Mvd2Fsa3Rocm91Z2guaHRtbAoKUUMgZmlsdGVyaW5nIGludm9sdmVzIGl0ZXJhdGl2ZSBhZGp1c3RtZW50cyB0byBkZXRlcm1pbmUgdGhlIG9wdGltYWwgdGhyZXNob2xkcyBmb3IgZmlsdGVyaW5nIGRlcHRoICh1bmlxdWUgbW9sZWN1bGFyIGlkZW50aWZpZXJzIFtVTUlzXSBwZXIgY2VsbCksIG1pdG9jaG9uZHJpYWwgKG1pdG8pIGZyYWN0aW9uIChwZXJjZW50YWdlIG9mIG1pdG9jaG9uZHJpYWwgZ2VuZSBleHByZXNzaW9uIHBlciBjZWxsKSwgYW5kIGRvdWJsZXRzICh0d28gb3IgbW9yZSBjZWxscyBjbHVzdGVyZWQgdG9nZXRoZXIpLiBGaW5hbCBjbHVzdGVyaW5nIGlzIG5vdCBjb25kdWN0ZWQgaW4gdGhpcyBzY3JpcHQsIGl0IGlzIGFkZHJlc3NlZCBpbiBhIHN1YnNlcXVlbnQgc2NyaXB0LCAiQ2x1c3RlcmluZ19hbmRfYW5ub3RhdGlvbiIuCgpJZiBhbWJpZW50IFJOQSBmaWx0ZXJpbmcgaXMgYXBwbGllZCwgaXQgaXMgcGVyZm9ybWVkIGZpcnN0LCBmb2xsb3dlZCBieSBmaWx0ZXJpbmcgYmFzZWQgb24gZGVwdGgsIG1pdG9jaG9uZHJpYWwgZnJhY3Rpb24sIGFuZCBkb3VibGV0cy4gQXQgdGhlIGVuZCBvZiB0aGUgcGlwZWxpbmUsIGEgbmV3IENSTWV0cmljcyBvYmplY3QgaXMgY29uc3RydWN0ZWQgdXNpbmcgdGhlIGZpbHRlcmVkIGNvdW50IG1hdHJpY2VzICg9Y21zKSB0byBwbG90IHRoZSB2aW9saW4gcGxvdHMgZm9yIFVNSSBhbmQgZ2VuZSBjb3VudHMgcG9zdC1maWx0ZXJpbmcuCgpEdWUgdG8gaXRzIGNvbXB1dGF0aW9uYWwgaW50ZW5zaXR5LCBhbWJpZW50IFJOQSBmaWx0ZXJpbmcgaXMgbm90IHBhcnQgb2YgdGhlIHN0YW5kYXJkIGFuYWx5c2lzIHBpcGVsaW5lIGJ1dCBpcyBwZXJmb3JtZWQgdXBvbiBzcGVjaWZpYyB1c2VyIHJlcXVlc3QuCgoKIyBTZXR1cApgYGB7ciwgbWVzc2FnZT1GfQpsaWJyYXJ5KGRldnRvb2xzKQojIGxvYWRpbmcgbG9jYWxseSBhZGFwdGVkIENSTWV0cmljcyB2ZXJzaW9uCiNsb2FkX2FsbCgiL3Blb3BsZS9ucnEzNjQvQ1JNZXRyaWNzX0xXIikKbGlicmFyeShDUk1ldHJpY3MpCmxpYnJhcnkobWFncml0dHIpCmxpYnJhcnkoZHBseXIpCmxpYnJhcnkoY29ub3MpCmxpYnJhcnkocGFnb2RhMikKbGlicmFyeShxcykKYGBgCiMgMS4gZmFzdFFDIGFuZCBtdWx0aVFDClByb3ZpZGUgbGlua3MgdG8gbXVsdGlRQyByZXBvcnRzIG9mIHJhdyBzZXF1ZW5jaW5nIGRhdGEgYW5kIGNlbGxyYW5nZXIgbWV0cmljcy4gRG9lcyBub3QgeWV0IHdvcmsgb24gY2VsbHJhbmdlciBtdWx0aSAoRkxFWCkgYW5kIFBhcnNlIG91dHB1dC4KCiMgMi4gQ3JlYXRlIENSTWV0cmljcyBvYmplY3QKSW4gY2FzZSBvZiBQYXJzZSBkYXRhIHVzZSBkYXRhLnBhdGggdG8gImNvbWJpbmVkIiBmb2xkZXIsIGZvciAxMHggRkxFWCBkYXRhLnBhdGggdG8gInBlcl9zYW1wbGVfb3V0cyIgZm9sZGVyLiAKTWV0YWRhdGEgc2hvdWxkIGF0IGxlYXN0IGNvbnRhaW4gInNhbXBsZSIgYW5kICJncm91cC9jb25kaXRpb24iIGJ1dCBjYW4gaGF2ZSBtb3JlIGNvbHVtbnMuCmBgYHtyfQpjcm0gPC0gQ1JNZXRyaWNzJG5ldyhkYXRhLnBhdGggPSAiL2RhdGEvY291bnRzL1twcm9qZWN0X25hbWVdLyIsIAogICAgICAgICAgICAgICAgICAgICBtZXRhZGF0YSA9ICJwYXRoX3RvX21ldGFkYXRhLmNzdiIsIAogICAgICAgICAgICAgICAgICAgICBuLmNvcmVzID0gNjApCmBgYAoKYGBge3J9CmNybSRtZXRhZGF0YQpgYGAKCmBgYHtyfQpxc2F2ZShjcm0sICJjcm0ucXMiLCBudGhyZWFkcyA9IDEwKQpgYGAKCiMgMy4gQmFzaWMgbWV0cmljcwoKU2hvdyBhdmFpbGFibGUgbWV0cmljcyB0byBwbG90LgpgYGB7cn0KY3JtJHNlbGVjdE1ldHJpY3MoKQpgYGAKCiMjIFBsb3Qgc2VsZWN0ZWQgbWV0cmljcyBwZXIgc2FtcGxlCmUuZy4gY2VsbHMgcGVyIHNhbXBsZSwgbWVkaWFuIGdlbmVzLCBtZWRpYW4gVU1JcywgbWVhbiByZWFkcwpgYGB7ciwgZmlnLmhlaWdodD02LCB3YXJuaW5nPUZ9Cm1ldHJpY3MudG8ucGxvdCA8LSBjcm0kc2VsZWN0TWV0cmljcyhpZHMgPSBjKDE6Myw1LDYpKQpjcm0kcGxvdFN1bW1hcnlNZXRyaWNzKHBsb3QuZ2VvbSA9ICJiYXIiLCBjb21wLmdyb3VwID0gInNhbXBsZSIsIG1ldHJpY3MgPSBtZXRyaWNzLnRvLnBsb3QpCmBgYAoKIyMgUGxvdCBzZWxlY3RlZCBtZXRyaWNzIHBlciBjb25kaXRpb24KV2UgY2FuIGRvIHRoZSBzYW1lLCBidXQgc2V0IHRoZSBjb21wYXJpc29uIGdyb3VwIHRvIGNvbmRpdGlvbi4gVGhpcyB3aWxsIGFkZCBzdGF0aXN0aWNzIHRvIHRoZSBwbG90cy4gQWRkaXRpb25hbHksIHdlIGNhbiBhZGQgYSBzZWNvbmQgY29tcGFyaXNvbiBncm91cCBmb3IgY29sb3JpbmcuCmBgYHtyLCBmaWcuaGVpZ2h0PTYsIHdhcm5pbmc9Rn0KY3JtJHBsb3RTdW1tYXJ5TWV0cmljcyhjb21wLmdyb3VwID0gImNvbmRpdGlvbiIsCiAgICAgICAgICAgICAgICAgICAgICAgbWV0cmljcyA9IG1ldHJpY3MudG8ucGxvdCwgCiAgICAgICAgICAgICAgICAgICAgICAgcGxvdC5nZW9tID0gInBvaW50IiwgCiAgICAgICAgICAgICAgICAgICAgICAgc3RhdC50ZXN0ID0gIm5vbi1wYXJhbWV0cmljIikgI3NlY29uZC5jb21wLmdyb3VwPSAic2V4IgpgYGAKCiMjIEFkZCBhbmQgcG90IGRldGFpbGVkIG1ldHJpY2VzCmBgYHtyfQpjcm0kYWRkQ21zKGFkZC5tZXRhZGF0YT1GKSAjZm9yIFBhcnNlIGRhdGE6IHBhcnNlID0gVCwgZm9yIEZMRVg6IGZsZXggPSBUCmNybSRhZGREZXRhaWxlZE1ldHJpY3Mobi5jb3JlcyA9IDMwKQpgYGAKCmdldCB0b3RhbCBudW1iZXIgb2Ygc2VxdWVuY2VkIG51Y2xlaQpgYGB7cn0Kc3VtKHNhcHBseShjcm0kY21zLCBmdW5jdGlvbihkKSBkaW0oZClbMl0pKQpgYGAKCnBsb3QgVU1JcyBhbmQgZ2VuZXMgcGVyIHNhbXBsZSBiZWZvcmUgZmlsdGVyaW5nLCBob3Jpem9udGFsIGxpbmVzIGluZGljYXRlIHRoZSBtZWRpYW4gdmFsdWVzIGZvciBhbGwgc2FtcGxlcwpgYGB7ciwgd2FybmluZz1GfQpjcm0kcGxvdERldGFpbGVkTWV0cmljcyhjb21wLmdyb3VwID0gImNvbmRpdGlvbiIsCiAgICAgICAgICAgICAgICAgICAgICAgIG1ldHJpY3MgPSBjKCJVTUlfY291bnQiLCAgImdlbmVfY291bnQiKSwgCiAgICAgICAgICAgICAgICAgICAgICAgIHBsb3QuZ2VvbSA9ICJ2aW9saW4iKQpgYGAKR2VuZXJhdGUgYSBkYXRhIGZyYW1lIHdpdGggdGhlIG1ldHJpY3MuCmBgYHtyfQptZXRyaWNzIDwtIGNybSRzdW1tYXJ5Lm1ldHJpY3MKbWV0cmljcyAlPD4lIGFzLmRhdGEuZnJhbWUoKQptZXRyaWNzCmBgYApHZXQgbWVkaWFuIGdlbmVzIHBlciBjZWxsIGFjcm9zcyBhbGwgc2FtcGxlcyBiZWZvcmUgZmlsdGVyaW5nCihpbiBjYXNlIG9mIFBhcnNlOiAiaGczOF9tZWRpYW5fZ2VuZXNfcGVyX2NlbGwiKS4KYGBge3J9Cm1lZGlhbihtZXRyaWNzW21ldHJpY3MkbWV0cmljPT0ibWVkaWFuIGdlbmVzIHBlciBjZWxsIixdJHZhbHVlKQpgYGAKCkdldCBtZWRpYW4gVU1JcyBwZXIgY2VsbCBhY3Jvc3MgYWxsIHNhbXBsZXMgYmVmb3JlIGZpbHRlcmluZwooaW4gY2FzZSBvZiBQYXJzZTogImhnMzhfbWVkaWFuX3RzY3BfcGVyX2NlbGwiKS4KYGBge3J9Cm1lZGlhbihtZXRyaWNzW21ldHJpY3MkbWV0cmljPT0ibWVkaWFuIHVtaSBjb3VudHMgcGVyIGNlbGwiLF0kdmFsdWUpCmBgYAoKSWYgYW1iaWVudCBSTkEgcmVtb3ZhbCBpcyBwYXJ0IG9mIHRoZSBwaXBlbGluZSwgaXQgaXMgcGVyZm9ybWVkIGhlcmUuIFJlbGV2YW50IGNvZGUgaXMgbG9jYXRlZCBmdXJ0aGVyIGRvd24gaW4gdGhlIHNjcmlwdCB1bmRlciA4LgoKIyA0LiBFbWJlZCBjZWxscyB1c2luZyBjb25vcyAKVG8gcGxvdCB0aGUgY2VsbHMgaW4gYW4gZW1iZWRkaW5nLCB3ZSBuZWVkIHRvIHByZXByb2Nlc3MgdGhlIHJhdyBjb3VudCBtYXRyaWNlcyB3aXRoIHBhZ29kYTIgKHNldXJhdCBpcyBhbHNvIHBvc3NpYmxlKS4KYGBge3J9CmNybSRkb1ByZXByb2Nlc3NpbmcoKSAjbWluLnRyYW5zY3JpcHRzLnBlci5jZWxsID0gMTAwCmBgYAoKQ3JlYXRlIGVtYmVkZGluZyB1c2luZyBjb25vcy4gVGhpcyBmdW5jdGlvbnMgZXhlY3V0ZXMgdGhlIGZ1bmN0aW9ucyBDb25vcyRuZXcoY21zLCBuLmNvcmVzID0gbi5jb3JlcyksIGJ1aWxkR3JhcGgoKSwgZmluZENvbW11bml0aWVzKCksIGVtYmVkR3JhcGgobWV0aG9kID0gIlVNQVAiKSwgd2l0aCBkZWZhdWx0IHNldHRpbmdzLCB3aGljaCBjYW4gYmUgYWRqdXN0ZWQuIApgYGB7cn0KY3JtJGNyZWF0ZUVtYmVkZGluZygpCmBgYAoKUGxvdCB0aGUgZW1iZWRkaW5nLiBUaGlzIGlzIGJlZm9yZSBmaWx0ZXJpbmcsIHNvIHdlIGFyZSBub3QgeWV0IGludGVyZXN0ZWQgaW4gdGhlIGNsdXN0ZXJzLCB0aG9zZSBhcmUgcmVmaW5lZCBpbiB0aGUgbmV4dCBzY3JpcHQuCmBgYHtyfQpjcm0kcGxvdEVtYmVkZGluZygpCmBgYAoKIyA1LiBGaWx0ZXJpbmcgb3V0IGxvdyBxdWFsaXR5IGNlbGxzCiMjIENlbGwgZGVwdGgKUGxvdCBjZWxsIGRlcHRoIGluIHRoZSBlbWJlZGRpbmcgb3IgYXMgaGlzdG9ncmFtcyBwZXIgc2FtcGxlLgpgYGB7cn0KY3JtJHBsb3RFbWJlZGRpbmcoZGVwdGggPSBUUlVFLCAKICAgICAgICAgICAgIGRlcHRoLmN1dG9mZiA9IDEwMDApCmBgYApgYGB7ciwgZmlnLmhlaWdodD02LCB3YXJuaW5nPUZ9CmNybSRwbG90RGVwdGgoKQpgYGAKCklmIHRoZSBkZXB0aCBkaXN0cmlidXRpb24gdmFyaWVzIGJldHdlZW4gc2FtcGxlcywgd2UgY2FuIHVzZSBzYW1wbGUtc3BlY2lmaWMgY3V0b2Zmcy4KYGBge3J9CmRlcHRoX2N1dG9mZl92ZWMgPC0gYygxZTMsIDFlMywgNTAwLCAxZTMsIDFlMywgNTAwLCAxZTMsIDFlMywgMWUzLCAxZTMsIDFlMywgNTAwKSAlPiUgCiAgc2V0TmFtZXMoY3JtJGRldGFpbGVkLm1ldHJpY3Mkc2FtcGxlICU+JSAKICAgICAgICAgICAgIHVuaXF1ZSgpICU+JSAKICAgICAgICAgICAgIHNvcnQoKSkKCmRlcHRoX2N1dG9mZl92ZWMKYGBgCgpQbG90IHNhbXBsZSBzcGVjaWZpYyBjdXRvZmYgaW4gaGlzdG9ncmFtIGFuZCBlbWJlZGRpbmcuCmBgYHtyLCB3YXJuaW5nPUYsIGZpZy5oZWlnaHQ9Nn0KY3JtJHBsb3REZXB0aChjdXRvZmYgPSBkZXB0aF9jdXRvZmZfdmVjKQpgYGAKCmBgYHtyfQpjcm0kcGxvdEVtYmVkZGluZyhkZXB0aCA9IFRSVUUsIAogICAgICAgICAgICAgZGVwdGguY3V0b2ZmID0gZGVwdGhfY3V0b2ZmX3ZlYykKYGBgCgojIyBNaXRvY2hvbmRyaWFsIGdlbmUgZnJhY3Rpb24KSW52ZXN0aWdhdGUgdGhlIG1pdG9jaG9uZHJpYWwgZ2VuZSBmcmFjdGlvbiBpbiB0aGUgY2VsbHMuIFRoZSBleGFtcGxlIGRhdGFzZXQgaGFzIGEgdmVyeSBsb3cgZXhwcmVzc2lvbiBvZiBtaXRvY2hvbmRyaWFsIGdlbmVzLiBVc3VhbCBjdXRvZmYgaXMgNSUgb3IgaGlnaGVyLgpgYGB7cn0KY3JtJHBsb3RFbWJlZGRpbmcobWl0by5mcmFjID0gVFJVRSwgCiAgICAgICAgICAgICBtaXRvLmN1dG9mZiA9IDAuMDEsIAogICAgICAgICAgICAgc3BlY2llcyA9ICJtb3VzZSIpCmBgYAoKV2UgY2FuIHBsb3QgdGhlIGRpc3RyaWJ1dGlvbiBvZiB0aGUgbWl0b2Nob25kcmlhbCBmcmFjdGlvbiBwZXIgc2FtcGxlIGFuZCBhbHNvIGluY2x1ZGUgc2FtcGxlLXdpc2UgY3V0b2ZmcyAobm90IHNob3duIGhlcmUgYXMgdGhlIGV4YW1wbGUgZGF0YXNldCBoYXMgb25seSBhIHZlcnkgbG93IGV4cHJlc3Npb24gb2YgbWl0b2Nob25kcmlhbCBnZW5lcykuCmBgYHtyLCBlY2hvPVRSVUUsIHJlc3VsdHM9J2hpZGUnLCBmaWcuaGVpZ2h0PTYsIHdhcm5pbmc9Rn0KY3JtJHBsb3RNaXRvRnJhY3Rpb24oY3V0b2ZmID0gMC4wMSkKYGBgCgojIyBEb3VibGV0IGRldGVjdGlvbgpEb3VibGV0IGRldGVjdGlvbiB3aXRoaW4gQ1JNZXRyaWNzIGlzIHBvc3NpYmxlIHdpdGggdGhlIHB5dGhvbiBwYWNrYWdlcyBzY3J1YmxldCBvciBEb3VibGV0RGV0ZWN0aW9uClNjcnVibGV0IGlzIHRoZSBkZWZhdWx0IG1ldGhvZCwgd2hpY2ggaXMgZmFzdC4gRG91YmxldERldGVjdGlvbiBpcyBzaWduaWZpY2FudGx5IHNsb3dlciwgYnV0IHBlcmZvcm1zIGJldHRlciBhY2NvcmRpbmcgdG8gdGhpcyByZXZpZXcgKGh0dHBzOi8vd3d3LnNjaWVuY2VkaXJlY3QuY29tL3NjaWVuY2UvYXJ0aWNsZS9waWkvUzI0MDU0NzEyMjAzMDQ1OTIpLiBXaGNpaCBvbmUgdG8gY2hvb3NlIGRlcGVuZHMgb24gdGhlIGRhdGFzZXQgYW5kIHRlY2hub2xvZ3kgdXNlZC4KCkhlcmUgd2UgdXNlIHNjcnVibGV0IGFzIGRlZmF1bHQgbWV0aG9kLgpBIHZpcnR1YWwgZW52aXJvbm1lbnQgaXMgdXNlZCB0byBpbnN0YWxsIHNjcnVibGV0LgpgYGB7cn0KbGlicmFyeShyZXRpY3VsYXRlKQpjb25kYV9jcmVhdGUoInNjcnVibGV0IiwgCiAgICAgICAgICAgICBjb25kYSA9ICIvb3B0L3NvZnR3YXJlL21pbmljb25kYS80LjEyLjAvY29uZGFiaW4vY29uZGEiLCAKICAgICAgICAgICAgIHB5dGhvbl92ZXJzaW9uID0gMy44KQpgYGAKCgpSdW4gc2NydWJsZXQgZnJvbSB3aXRoaW4gUiBvciBnZW5lcmF0ZSBhIFB5dGhvbiBzY3JpcHQgdG8gcnVuIGl0IGZyb20gdGhlIHRlcm1pbmFsLiBUbyBkbyB0aGlzLCBzZXQgZXhwb3J0ID0gVFJVRSBhbmQgcnVuIHB5dGhvbiBzY3J1YmxldC5weSBpbnNpZGUgdGhlIHZpcnR1YWwgZW52aXJvbm1lbnQgdG8gZ2VuZXJhdGUgZGF0YS4KYGBge3J9CmNybSRkZXRlY3REb3VibGV0cyhlbnYgPSAic2NydWJsZXQiLAogICAgICAgICAgICAgICAgICAgY29uZGEucGF0aCA9ICIvb3B0L3NvZnR3YXJlL21pbmljb25kYS80LjEyLjAvY29uZGFiaW4vY29uZGEiLAogICAgICAgICAgICAgICAgICAgbWV0aG9kID0gInNjcnVibGV0IikKYGBgCgpUaGVuLCBsb2FkIGRhdGEgd2l0aDoKYGBge3J9CmNybSRhZGREb3VibGV0cygpCmBgYAoKUGxvdCB0aGUgZXN0aW1hdGVkIGRvdWJsZXRzIGluIHRoZSBlbWJlZGRpbmcuCmBgYHtyfQpjcm0kcGxvdEVtYmVkZGluZyhkb3VibGV0Lm1ldGhvZCA9ICJzY3J1YmxldCIpCmBgYAoKCiMjIFBsb3QgZmlsdGVyZWQgY2VsbHMKUGxvdCBhbGwgY2VsbHMgdG8gYmUgZmlsdGVyZWQgb24gdGhlIGVtYmVkZGluZy4KYGBge3J9CmNybSRwbG90RmlsdGVyZWRDZWxscyh0eXBlID0gImVtYmVkZGluZyIsIAogICAgICAgICAgICAgICAgICAgICAgZGVwdGggPSBUUlVFLCAKICAgICAgICAgICAgICAgICAgICAgIGRlcHRoLmN1dG9mZiA9IDEwMDAsIAogICAgICAgICAgICAgICAgICAgICAgZG91YmxldC5tZXRob2QgPSAic2NydWJsZXQiLCAKICAgICAgICAgICAgICAgICAgICAgIG1pdG8uZnJhYyA9IEZBTFNFLCAKICAgICAgICAgICAgICAgICAgICAgIHNwZWNpZXMgPSAibW91c2UiKQpgYGAKClBsb3QgdGhlIGNlbGxzIHRvIGJlIGZpbHRlcmVkIHBlciBzYW1wbGUgaW4gYSBiYXIgcGxvdCB3aGVyZSBjb21iaW5hdGlvbiBtZWFucyBhIGNlbGwgdGhhdCBoYXMgYXQgbGVhc3QgdHdvIGZpbHRlciBsYWJlbHMsIGUuZy4gZG91YmxldCBhbmQgZGVwdGguCmBgYHtyLCB3YXJuaW5nPUZ9CmNybSRwbG90RmlsdGVyZWRDZWxscyh0eXBlID0gImJhciIsIAogICAgICAgICAgICAgICAgICAgICAgZGVwdGggPSBUUlVFLCAKICAgICAgICAgICAgICAgICAgICAgIGRlcHRoLmN1dG9mZiA9IDEwMDAsIAogICAgICAgICAgICAgICAgICAgICAgZG91YmxldC5tZXRob2QgPSAic2NydWJsZXQiLCAKICAgICAgICAgICAgICAgICAgICAgIG1pdG8uZnJhYyA9IEZBTFNFLCAKICAgICAgICAgICAgICAgICAgICAgIHNwZWNpZXMgPSAibW91c2UiKQpgYGAKCgojIDYuIEZpbHRlciBhbmQgc2F2ZSBjb3VudCBtYXRyaWNlcwpGaW5hbGx5LCBmaWx0ZXIgdGhlIGNvdW50IG1hdHJpY2VzIHRvIGNyZWF0ZSBhIGNsZWFuZWQgbGlzdCB0byBiZSB1c2VkIGluIGRvd25zdHJlYW0gYXBwbGljYXRpb25zLgpJZiBjZWxsYmVuZGVyIHNob3VsZCBiZSB1c2VkIHRoZW4gY21zIG5lZWRzIHRvIGJlIGFkZGVkIGFmdGVyIGNlbGxiZW5kZXIgYW5kIHRoZW4gYWxsIHN1YnNlcXVlbnQgZmlsdGVyaW5nIGlzIGRvbmUuCmBgYHtyfQpjcm0kZmlsdGVyQ21zKGRlcHRoLmN1dG9mZiA9IDEwMDAsIAogICAgICAgICAgICAgIG1pdG8uY3V0b2ZmID0gTlVMTCwgCiAgICAgICAgICAgICAgZG91YmxldHMgPSAic2NydWJsZXQiLAogICAgICAgICAgICAgIHNhbXBsZXMudG8uZXhjbHVkZSA9IE5VTEwsCiAgICAgICAgICAgICAgc3BlY2llcyA9ICJtb3VzZSIsCiAgICAgICAgICAgICAgdmVyYm9zZSA9IFRSVUUpCmBgYAoKVGhlIGZpbHRlcmVkIGxpc3Qgb2YgY291bnQgbWF0cmljZXMgaXMgc3RvcmVkIGluICRjbXMuZmlsdGVyZWQgd2hpY2ggY2FuIGJlIHNhdmVkIG9uIGRpc2suCmBgYHtyfQpxc2F2ZShjcm0kY21zLmZpbHRlcmVkLCAiY21zX2ZpbHRlcmVkW19jZWxsYmVuZGVyXS5xcyIsIAogICAgICBudGhyZWFkcyA9IDEwKQpgYGAKCiMgNy4gUGxvdCBkZXRhaWxlZCBtZXRyaWNzIGFmdGVyIGZpbHRlcmluZwpgYGB7cn0KY21zIDwtIHFyZWFkKCJjbXNfZmlsdGVyZWRbX2NlbGxiZW5kZXJdLnFzIikKYGBgCgpDcmVhdGUgYSBuZXcgY3JtIG9iamVjdCB3aXRoIGNtcyBhZnRlciBmaWx0ZXJpbmcuCmBgYHtyfQpjcm1fZmlsdGVyZWQgPC0gQ1JNZXRyaWNzJG5ldyhjbXMgPSBjcm0kY21zLmZpbHRlcmVkLCBuLmNvcmVzID0gNjAsIG1ldGFkYXRhPSJwYXRoX3RvX21ldGFkYXRhLmNzdiIpCmNybV9maWx0ZXJlZCRhZGRTdW1tYXJ5RnJvbUNtcygpCmNybV9maWx0ZXJlZCRhZGREZXRhaWxlZE1ldHJpY3Mobi5jb3JlcyA9IDMwKQpgYGAKClRvdGFsIG51bWJlciBvZiBudWNsZWkgcmV0YWluZWQgYWZ0ZXIgUUM6CmBgYHtyfQpzdW0oc2FwcGx5KGNybV9maWx0ZXJlZCRjbXMsIGZ1bmN0aW9uKGQpIGRpbShkKVsyXSkpCmBgYAoKUGxvdCBVTUlzIGFuZCBnZW5lcyBwZXIgc2FtcGxlIGFmdGVyIFFDIGZpbHRlcmluZywgaG9yaXpvbnRhbCBsaW5lcyBpbmRpY2F0ZSB0aGUgbWVkaWFuIHZhbHVlcyBmb3IgYWxsIHNhbXBsZXMuCmBgYHtyfQpjcm1fZmlsdGVyZWQkcGxvdERldGFpbGVkTWV0cmljcyhjb21wLmdyb3VwID0gImNvbmRpdGlvbiIsCiAgICAgICAgICAgICAgICAgICAgICAgIG1ldHJpY3MgPSBjKCJVTUlfY291bnQiLCAgImdlbmVfY291bnQiKSwgCiAgICAgICAgICAgICAgICAgICAgICAgIHBsb3QuZ2VvbSA9ICJ2aW9saW4iKQpgYGAKR2VuZXJhdGUgYSBkYXRhIGZyYW1lIHdpdGggdGhlIG1ldHJpY3MuCmBgYHtyfQptZXRyaWNzIDwtIGNybV9maWx0ZXJlZCRzdW1tYXJ5Lm1ldHJpY3MKbWV0cmljcyAlPD4lIGFzLmRhdGEuZnJhbWUoKQptZXRyaWNzCmBgYApNZWRpYW4gZ2VuZXMgcGVyIGNlbGwgYWNyb3NzIGFsbCBzYW1wbGVzIGFmdGVyIFFDIGZpbHRlcmluZyAoaW4gY2FzZSBvZiBQYXJzZTogImhnMzhfbWVkaWFuX2dlbmVzX3Blcl9jZWxsIikuCmBgYHtyfQptZWRpYW4obWV0cmljc1ttZXRyaWNzJG1ldHJpYz09Im1lZGlhbiBnZW5lcyBwZXIgY2VsbCIsXSR2YWx1ZSkKYGBgCgpNZWRpYW4gVU1JcyBwZXIgY2VsbCBhY3Jvc3MgYWxsIHNhbXBsZXMgYWZ0ZXIgUUMgZmlsdGVyaW5nIChpbiBjYXMgb2YgUGFyc2U6ICJoZzM4X21lZGlhbl90c2NwX3Blcl9jZWxsIikuCmBgYHtyfQptZWRpYW4obWV0cmljc1ttZXRyaWNzJG1ldHJpYz09Im1lZGlhbiB1bWkgY291bnRzIHBlciBjZWxsIixdJHZhbHVlKQpgYGAKCgojIDguIE9wdGlvbmFsOiByZW1vdmUgYW1iaWVudCBSTkEKYGBge3IsIGluY2x1ZGU9Rn0KY3JtIDwtIHFyZWFkKCJjcm1fY2VsbGJlbmRlcl93aXRob3V0X2N0cmwzX2FuZF90cmVhdGVkNS5xcyIsIG50aHJlYWRzPTEwKQpgYGAKCldlIHVzZSB1c3VhbGx5IGNlbGxiZW5kZXIsIFNvdXBYIGlzIGFsc28gaW5jbHVkZWQgaW4gQ1JNZXRyaWNzLgpQZXJmb3JtIHRoaXMgc3RlcCBiZWZvcmUgZmlsdGVyaW5nLCBpdCBjaGFuZ2VzIHRoZSBudW1iZXIgb2YgY2FsbGVkIGNlbGxzIGFuZCB0aGVpciBnZW5lIGNvdW50cy4KCkluIHRoZSBvbGQgY2VsbGJlbmRlciB2ZXJzaW9uIChiZWZvcmUgMC4zLjApIHdlIG5lZWRlZCB0byBzcGVjaWZ5IGV4cGVjdGVkIG51bWJlciBvZiBjZWxscyBhbmQgdG90YWwgZHJvcGxldHMgaW5jbHVkZWQuIEFzIGhpbnRlZCBpbiB0aGUgbWFudWFsLCB0aGUgbnVtYmVyIG9mIHRvdGFsIGRyb3BsZXRzIGluY2x1ZGVkIGNvdWxkIGJlIGV4cGVjdGVkIG51bWJlciBvZiBjZWxscyBtdWx0aXBsaWVkIGJ5IDMsIHRoaXMgY2FuIHN0aWxsIGJlIHBsb3R0ZWQgaGVyZSB3aGVuIHNldHRpbmcgc2hvdy50b3RhbC5kcm9wbGV0cz1ULgpBZGRpdGlvbmFsbHksIGluIGNhc2Ugb2YgUGFyc2UsIHRoZSBmb2xsb3dpbmcgZnVuY3Rpb24gY3JlYXRlcyB0aGUgaW5wdXQgZmlsZXMgZm9yIGNlbGxiZW5kZXIgZnJvbSB0aGUgUGFyc2Ugb3V0cHV0LiAKYGBge3IsIGZpZy5oZWlnaHQ9Niwgd2FybmluZz1GfQpjcm0kcHJlcGFyZUNlbGxiZW5kZXIoc2hvdy5leHBlY3RlZC5jZWxscyA9IFQsIHNob3cudG90YWwuZHJvcGxldHMgPSBUKQpgYGAKCgpUaGUgZm9sbG93aW5nIGZ1bmN0aW9uIHByZXBhcmVzIGFuZCBzYXZlcyB0aGUgY2VsbGJlbmRlciBzY3JpcHQuIFRvIG9ubHkgc2VsZWN0IHNwZWNpZmljIHNhbXBsZXMsIHVzZSB0aGUgc2FtcGxlcyBwYXJhbWV0ZXIuIAp0b3RhbC5kcm9wbGV0cyBhbmQgZXhwZWN0ZWQuY2VsbHMgaXMgYSBsb2dpY2FsIGJlY2F1c2UgaW4gY2VsbGJlbmRlciBmcm9tIHYwLjMuMCB0b3RhbC5kcm9wbGV0cyBhbmQgZXhwZWN0ZWQuY2VsbHMgZG8gbm90IG5lZWQgdG8gYmUgc3BlY2lmaWVkIGFueW1vcmUuIDxicj4KCQnil4sgRkFMU0UgaXMgZGVmYXVsdCA8YnI+CgkJ4peLIFRSVUUgd291bGQgdGFrZSBleHBlY3RlZC5jZWxscyBmcm9tIG1ldGFkYXRhIGFuZCB0b3RhbCBkcm9wbGV0cyB3b3VsZCBiZSAzeCBleHBlY3RlZCBjZWxscyA8YnI+CgkJ4peLIGFsdGVybmF0aXZlbHkgZ2l2ZSBvd24gbnVtYmVycyAobmFtZWQgdmVjdG9yKSAKYGBge3J9CmNybSRzYXZlQ2VsbGJlbmRlclNjcmlwdChmaWxlID0gImNlbGxiZW5kZXJfc2NyaXB0LnNoIiwgCiAgICAgICAgICAgICAgICAgICAgICAgICBmcHIgPSAwLjAxLCAKICAgICAgICAgICAgICAgICAgICAgICAgIGVwb2NocyA9IDE1MCwgCiAgICAgICAgICAgICAgICAgICAgICAgICB1c2UuZ3B1ID0gVFJVRSwgdG90YWwuZHJvcGxldHMgPSBGLCBleHBlY3RlZC5jZWxscyA9IEYpICMgY291bGQgYmUgdG90YWwuZHJvcGxldHMgPSBkcm9wbGV0cyAobmFtZWQgdmVjdG9yKQpgYGAKCldlIGNhbiBydW4gdGhpcyBzY3JpcHQgaW4gdGhlIHRlcm1pbmFsIChzY3JlZW4gc2Vzc2lvbikuIEhlcmUsIHdlIGFjdGl2YXRlIHRoZSBlbnZpcm9ubWVudDogY29uZGEgYWN0aXZhdGUgY2VsbGJlbmRlcl92MC4zLjAgYW5kIHdlIHJ1biB0aGUgYmFzaCBzY3JpcHQ6IHNoIGNlbGxiZW5kZXJfc2NyaXB0LnNoLgoKIyMgUGxvdCBjZWxsYmVuZGVyIHJlc3VsdHMKUGxvdCB0aGUgY2hhbmdlcyBpbiBjZWxsIG51bWJlcnMgZm9sbG93aW5nIENlbGxCZW5kZXIgZXN0aW1hdGlvbnMuCmBgYHtyLCB3YXJuaW5nPUZ9CmNybSRwbG90Q2JDZWxscygpCmBgYApgYGB7ciwgZXZhbD1GLCBpbmNsdWRlPUZ9CmRldnRvb2xzOjpsb2FkX2FsbCgiL3Blb3BsZS9ucnEzNjQvQ1JNZXRyaWNzX0xXIikKY3JtICU8PiUgQ1JNZXRyaWNzJG5ldygpCmBgYAoKUGxvdCB0aGUgQ2VsbEJlbmRlciB0cmFpbmluZyByZXN1bHRzLgpgYGB7ciwgZmlnLmhlaWdodD02LCB3YXJuaW5nPUZ9CmNybSRwbG90Q2JUcmFpbmluZygpCmBgYAoKUGxvdCB0aGUgY2VsbCBwcm9iYWJpbGl0aWVzLgpgYGB7ciwgZmlnLmhlaWdodD02LCB3YXJuaW5nPUZ9CmNybSRwbG90Q2JDZWxsUHJvYnMoKQpgYGAKClBsb3QgdGhlIGlkZW50aWZpZWQgYW1iaWVudCBnZW5lcyBwZXIgc2FtcGxlLgpgYGB7ciwgZmlnLmhlaWdodD02fQpjcm0kcGxvdENiQW1iRXhwKGN1dG9mZiA9IDAuMDA1KQpgYGAKCkxhc3RseSwgcGxvdCB0aGUgcHJvcG9ydGlvbiBvZiBzYW1wbGVzIGV4cHJlc3NpbmcgYW1iaWVudCBnZW5lcy4gTUFMQVQxIGlzIGlkZW50aWZpZWQgYXMgYW4gYW1iaWVudCBnZW5lIGluIGFsbW9zdCBhbGwgc2FtcGxlcyB3aGljaCBpcyBleHBlY3RlZC4KYGBge3IsIHdhcm5pbmc9Rn0KY3JtJHBsb3RDYkFtYkdlbmVzKGN1dG9mZiA9IDAuMDA1KQpgYGAKCiMjIEFkZCBjZWxsYmVuZGVyIGZpbHRlcmVkIGNtcyAKVGhlbiwgd2UgY2FuIGFkZCB0aGUgY2VsbGJlbmRlciBmaWx0ZXJlZCBjbXMgdG8gb3VyIG9iamVjdC4gQWRkaXRpb25hbGx5LCBpdCBpcyByZWNvbW1lbmRlZCB0byBnZW5lcmF0ZSBuZXcgc3VtbWFyeSBtZXRyaWNzIGZyb20gdGhlIGFkanVzdGVkIGNtcyB3aGljaCBjYW4gdGhlbiBiZSBwbG90dGVkLgpgYGB7cn0KY3JtJGNtcyA8LSBOVUxMCmNybSRhZGRDbXMoY2VsbGJlbmRlciA9IFQpCmNybSRhZGRTdW1tYXJ5RnJvbUNtcygpCmNybSRkZXRhaWxlZC5tZXRyaWNzIDwtIE5VTEwKY3JtJGFkZERldGFpbGVkTWV0cmljcygpCnFzYXZlKGNybSwgImNybV9jZWxsYmVuZGVyLnFzIikKYGBgCg==